using System;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Web;
using NWrapper.Attributes;
using NWrapper.Config;
using NWrapper.Entities;

namespace NWrapper.Repositories
{
	/// <summary>
	/// A proxy repository.
	/// </summary>
	public sealed class RepositoryProxy : IRepository, IDisposable
	{
		private Dictionary<string, IRepository> _repositories;
		private Dictionary<Type, string> _entities;
		private bool _disposed = false;

		#region Constructors
		/// <summary>
		/// Constructs the proxy with no initial settings.
		/// </summary>
		public RepositoryProxy()
		{
			_repositories = new Dictionary<string, IRepository>();
			_entities = new Dictionary<Type, string>();
		}
		/// <summary>
		/// Constructs the proxy using the given settings.
		/// </summary>
		/// <param name="settings">The settings to use by the proxy.</param>
		public RepositoryProxy(ConfigurationElementCollection<RepositorySettings> settings)
		{
			_repositories = new Dictionary<string, IRepository>();
			_entities = new Dictionary<Type, string>();

			foreach (RepositorySettings repository in settings)
			{
				Initialize(repository);
			}
		}
		#endregion

		#region IRepository Members
		/// <summary>
		/// Initializes a repository and adds it to the proxy.
		/// If the name of the repository specified in <paramref name="settings"/> is already
		/// in use, the current repository instance is removed prior to adding the new one.
		/// </summary>
		/// <param name="settings">The settings of the repository being initialized.</param>
		public void Initialize(RepositorySettings settings)
		{
			if (_repositories.ContainsKey(settings.Name))
			{
				RemoveRepository(settings.Name);
			}

			AddRepository(settings);
		}

		#region Retrieve
		/// <summary>
		/// Retrieves a collection of all records of a given type from the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <returns>A collection of objects.</returns>
		public ICollection<T> Retrieve<T>()
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Retrieve<T>();
		}
		/// <summary>
		/// Retrieves a collection of records of a given type that match the given criteria.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> Retrieve<T>(string whereClause)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Retrieve<T>(whereClause);
		}
		/// <summary>
		/// Retrieves a collection of records of a given type that match the given criteria, ordered by the given sort order.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <param name="sortClause">The sort order.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> Retrieve<T>(string whereClause, string sortClause)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Retrieve<T>(whereClause, sortClause);
		}
		/// <summary>
		/// Retrieves a collection of records of a given type that match the given criteria, ordered by the given sort order.
		/// The subset uses the given start index and number of rows after sorting.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <param name="sortClause">The sort order.</param>
		/// <param name="startRowIndex">The first row to retrieve.</param>
		/// <param name="maximumRows">The maximum number of rows to retrieve.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> Retrieve<T>(string whereClause, string sortClause, int startRowIndex, int maximumRows)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Retrieve<T>(whereClause, sortClause, startRowIndex, maximumRows);
		}
		#endregion

		#region RetrieveFirst
		/// <summary>
		/// Retrieves the first record of a given type from the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <returns>The object retrieved, or null if none.</returns>
		public T RetrieveFirst<T>()
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrieveFirst<T>();
		}
		/// <summary>
		/// Retrieves the first record of a given type from the repository that matches the given criteria.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <returns>The object retrieved, or null if none.</returns>
		public T RetrieveFirst<T>(string whereClause)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrieveFirst<T>(whereClause);
		}
		/// <summary>
		/// Retrieves the first record of a given type from the repository that matches the given criteria.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <param name="sortClause">The sort order.</param>
		/// <returns>The object retrieved, or null if none.</returns>
		public T RetrieveFirst<T>(string whereClause, string sortClause)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrieveFirst<T>(whereClause, sortClause);
		}
		#endregion

		#region RetrievePage
		/// <summary>
		/// Retrieves collection of records of a given type from the repository, using a given page size and index.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="pageSize">The size of each page.</param>
		/// <param name="pageIndex">The page number to retrieve.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> RetrievePage<T>(int pageSize, int pageIndex)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrievePage<T>(pageSize, pageIndex);
		}
		/// <summary>
		/// Retrieves collection of records of a given type from the repository, using a given page size and index.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="sortClause">The sort order.</param>
		/// <param name="pageSize">The size of each page.</param>
		/// <param name="pageIndex">The page number to retrieve.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> RetrievePage<T>(string sortClause, int pageSize, int pageIndex)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrievePage<T>(sortClause, pageSize, pageIndex);
		}
		/// <summary>
		/// Retrieves collection of records of a given type from the repository, using a given page size and index.
		/// </summary>
		/// <typeparam name="T">The type of object to retrieve.</typeparam>
		/// <param name="whereClause">The match criteria.</param>
		/// <param name="sortClause">The sort order.</param>
		/// <param name="pageSize">The size of each page.</param>
		/// <param name="pageIndex">The page number to retrieve.</param>
		/// <returns>A collection of objects</returns>
		public ICollection<T> RetrievePage<T>(string whereClause, string sortClause, int pageSize, int pageIndex)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.RetrievePage<T>(whereClause, sortClause, pageSize, pageIndex);
		}
		#endregion

		#region Save
		/// <summary>
		/// Saves an object to the repository.
		/// </summary>
		/// <typeparam name="T">The type of object.</typeparam>
		/// <param name="instance">The object to save.</param>
		public void Save<T>(T instance)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Save<T>(instance);
		}
		/// <summary>
		/// Saves an object to the repository.
		/// </summary>
		/// <typeparam name="T">The type of object.</typeparam>
		/// <param name="instance">The object to save.</param>
		/// <param name="includeChildren">Specify true to recursively save all child objects.</param>
		public void Save<T>(T instance, bool includeChildren)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Save<T>(instance, includeChildren);
		}
		/// <summary>
		/// Saves a collection of objects to the repository.
		/// </summary>
		/// <typeparam name="T">The type of object.</typeparam>
		/// <param name="instances">The collection of objects to save.</param>
		public void Save<T>(IEnumerable<T> instances)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Save<T>(instances);
		}
		/// <summary>
		/// Saves a collection of objects to the repository.
		/// </summary>
		/// <typeparam name="T">The type of object.</typeparam>
		/// <param name="instances">The collection of objects to save.</param>
		/// <param name="includeChildren">Specify true to recursively save all child objects.</param>
		public void Save<T>(IEnumerable<T> instances, bool includeChildren)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Save<T>(instances, includeChildren);
		}
		#endregion

		#region Delete
		/// <summary>
		/// Deletes an object from the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to delete.</typeparam>
		/// <param name="instance">The object to delete.</param>
		public void Delete<T>(T instance)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Delete<T>(instance);
		}
		/// <summary>
		/// Deletes a collection of objects from the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to delete.</typeparam>
		/// <param name="instances">The collection of objects to delete.</param>
		public void Delete<T>(IEnumerable<T> instances)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Delete<T>(instances);
		}
		/// <summary>
		/// Deletes objects from the repository using the given criteria.
		/// </summary>
		/// <typeparam name="T">The type of object to delete.</typeparam>
		/// <param name="whereClause">The criteria specifying objects to delete.</param>
		/// <returns>The number of objects deleted.</returns>
		public int Delete<T>(string whereClause)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Delete<T>(whereClause);
		}
		#endregion

		#region Insert
		/// <summary>
		/// Inserts a new object into the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to insert.</typeparam>
		/// <param name="instance">The object to insert.</param>
		public void Insert<T>(T instance)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			repo.Insert<T>(instance);
		}
		#endregion

		#region Resync
		/// <summary>
		/// Reloads an object from the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to resync.</typeparam>
		/// <param name="obj">The object to resync.</param>
		/// <returns>The resynced object.</returns>
		public T Resync<T>(T obj)
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.Resync<T>(obj);
		}
		#endregion

		#region GetObjectCount
		/// <summary>
		/// Gets the total number of objects of a given type that exist in the repository.
		/// </summary>
		/// <typeparam name="T">The type of object to count.</typeparam>
		/// <returns>The number of objects.</returns>
		public int GetObjectCount<T>()
		{
			IRepository repo = GetRepositoryForType(typeof(T));
			return repo.GetObjectCount<T>();
		}
		#endregion

		#endregion

		#region Public Methods
		/// <summary>
		/// Adds a repository and its entity mappings to the proxy using the given settings.
		/// </summary>
		/// <param name="settings">The settings of the repository to add.</param>
		public void AddRepository(RepositorySettings settings)
		{
			string assemblyName;
			string className;

			switch (settings.Type)
			{
				case RepositoryType.None:
					assemblyName = "NWrapper";
					className = "NWrapper.Repositories.NullRepository";
					break;
				case RepositoryType.Custom:
					assemblyName = settings.Assembly;
					className = settings.Class;
					break;
				case RepositoryType.WilsonORMapper:
					assemblyName = "NWrapper.Repositories.WilsonORMapper";
					className = "NWrapper.Repositories.WilsonORMapper.WilsonORMapperRepository";
					break;
				default:
					throw new ConfigurationErrorsException("Specified repository type in configuration is unknown or invalid");
			}

			IRepository repo = ProxyHelper.GetObject<IRepository>(assemblyName, className);
			repo.Initialize(settings);
			AddEntities(settings.Name, settings.Entities);

			_repositories.Add(settings.Name, repo);
		}
		/// <summary>
		/// Removes a single repository from the proxy. First, all entity references to this repository are removed,
		/// then the repository itself is removed. If the repository implements <see cref="IDisposable"/>, the 
		/// repository is properly disposed.
		/// </summary>
		/// <param name="name">The name of the repository to remove.</param>
		public void RemoveRepository(string name)
		{
			foreach (KeyValuePair<Type, string> entity in _entities)
			{
				if (entity.Value == name)
				{
					_entities.Remove(entity.Key);
				}
			}

			IRepository repo = _repositories[name];
			if (repo != null && repo is IDisposable)
			{
				((IDisposable)repo).Dispose();
			}

			_repositories.Remove(name);
		}
		/// <summary>
		/// Removes all repositories from the proxy by recursively calling <see cref="RemoveRepository"/> for
		/// each repository in the proxy.
		/// </summary>
		private void RemoveAllRepositories()
		{
			foreach (string repo in _repositories.Keys)
			{
				RemoveRepository(repo);
			}
		}
		/// <summary>
		/// Gets the type of the repository for the entity type.
		/// </summary>
		/// <param name="type">The type of entity.</param>
		/// <returns></returns>
		public IRepository GetRepositoryForType(Type type)
		{
			string repoName = _entities[type];
			if (String.IsNullOrEmpty(repoName))
				throw new RepositoryException("Type '" + type.ToString() + "' is not referenced in the registry.");

			IRepository repo = _repositories[repoName];
			if (repo == null)
				throw new RepositoryException(String.Format(
					"Repository '{0}' is referenced by type '{1}' but has not been referenced in the registry.",
					repoName, type.ToString()));

			return repo;
		}
		#endregion

		#region Private Methods
		private void AddEntities(string repositoryName, ConfigurationElementCollection<EntitySettings> entities)
		{
			Queue<Type> entityTypes = new Queue<Type>();
			string assemblyName;

			foreach (EntitySettings entity in entities)
			{
				assemblyName = entity.Assembly;

				if (HttpContext.Current != null)
				{
					assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
				}
				if (!assemblyName.EndsWith(".dll"))
				{
					assemblyName += ".dll";
				}

				Assembly assembly = Assembly.LoadFrom(assemblyName);
				if (assembly == null)
					throw new RepositoryException("Unable to load assembly " + assemblyName);

				Type[] types = assembly.GetTypes();
				foreach (Type type in types)
				{
					if (type.IsDefined(typeof(EntityAttribute), true))
					{
						EntityAttribute attrib = (EntityAttribute)type.GetCustomAttributes(typeof(EntityAttribute), true)[0];
						if (attrib.Name.Equals(repositoryName))
						{
							_entities.Add(type, repositoryName);
						}
					}
				}
			}
		}
		#endregion

		#region IDisposable Members
		/// <summary>
		/// Disposes the proxy and any underlying repositories.
		/// </summary>
		public void Dispose()
		{
			Dispose(true);
			GC.SuppressFinalize(this);
		}
		private void Dispose(bool disposing)
		{
			if (_disposed)
				return;

			if (disposing)
			{
				RemoveAllRepositories();
			}

			_disposed = true;
		}
		#endregion
	}
}
